package com.jzo2o.market.service.impl;

import ch.qos.logback.core.rolling.helper.IntegerTokenConverter;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.*;
import com.jzo2o.market.constants.TabTypeConstants;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.mapper.ActivityMapper;
import com.jzo2o.market.mapper.CouponMapper;
import com.jzo2o.market.mapper.CouponWriteOffMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.jzo2o.market.model.dto.request.ActivityQueryForPageReqDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponWriteOffService;
import com.jzo2o.mysql.utils.PageUtils;
import io.netty.util.collection.LongObjectHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;
import static com.jzo2o.market.enums.ActivityStatusEnum.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author itcast
 * @since 2023-09-16
 */
@Service
public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService {
    private static final int MILLION = 1000000;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private ICouponService couponService;

    @Resource
    private ICouponWriteOffService couponWriteOffService;

    @Autowired
    private CouponMapper couponMapper;

    @Autowired
    private CouponWriteOffMapper couponWriteOffMapper;

    /**
     * 新增优惠卷
     *
     * @param dto
     */
    @Override
    public void saveOrUpdateActivity(ActivitySaveReqDTO dto) {
        //首先判断这个前端传来的值全不全
        //库存和状态都没有需要我们自行赋值
        Activity activity = BeanUtil.copyProperties(dto, Activity.class);
        //库存数量
        activity.setStockNum(dto.getTotalNum());
        //设置第一次的状态--->后面这个是常量类
        activity.setStatus(NO_DISTRIBUTE.getStatus());
        this.saveOrUpdate(activity);
    }

    /**
     * 根据条件查询优惠卷活动列表
     *
     * @param dto
     * @return
     */
    @Override
    public PageResult<ActivityInfoResDTO> findByPage(ActivityQueryForPageReqDTO dto) {
        //我有什么？我有活动ID，活动名称，类型，优惠卷配置状态
        //我需要拿到什么？比activity而已，少了writeoffnum，receiveNum，剩下了个库存
        Page<Activity> page = PageUtils.parsePageQuery(dto, Activity.class);
        LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ObjectUtil.isNotEmpty(dto.getId()), Activity::getId, dto.getId())
                .eq(StrUtil.isNotEmpty(dto.getName()), Activity::getName, dto.getName())
                .eq(ObjectUtil.isNotEmpty(dto.getId()), Activity::getType, dto.getType())
                .eq(ObjectUtil.isNotEmpty(dto.getId()), Activity::getStatus, dto.getStatus());
        page = this.page(page, wrapper);
        //判断结果是否为空，如果为空也给前端传递一个空集合
        if(CollUtil.isEmpty(page.getRecords())) {
            return new PageResult<ActivityInfoResDTO>(0L, 0L, List.of());
        }
        //准备拼接参数
        //首先第一步，需要拿到全部的ID，writeOffNum:优惠卷核销数量，要去查核销表
        //receiveNum:优惠卷领取数量要根据活动ID去查优惠卷表
        List<Long> activityIds = page.getRecords().stream().map(Activity::getId).collect(Collectors.toList());
        //查优惠卷表
        Map<Long, Map<String, Long>> couponMaps = couponMapper.selectReceiveNumByIds(activityIds);
        //查核销表
        Map<Long, Map<String, Long>> couponWriteOffMaps = couponWriteOffMapper.selectWriteOffNumByIds(activityIds);

        List<ActivityInfoResDTO> list = page.getRecords().stream().map(e -> {
            ActivityInfoResDTO res = BeanUtil.copyProperties(e, ActivityInfoResDTO.class);
            res.setReceiveNum(couponMaps.getOrDefault(res.getId(), MapUtil.of()).getOrDefault("num", 0L).intValue());
            res.setWriteOffNum(couponWriteOffMaps.getOrDefault(res.getId(), MapUtil.of()).getOrDefault("num", 0L).intValue());
            return res;
        }).collect(Collectors.toList());
        return new PageResult<ActivityInfoResDTO>(page.getPages(), page.getTotal(), list);
    }

    /**
     * 查询优惠卷活动详情
     *
     * @param id
     * @return
     */
    @Override
    public ActivityInfoResDTO findById(Long id) {
        //1. 根据活动id查询activity表
        Activity activity = this.getById(id);
        if (ObjectUtil.isNull(activity)){
            throw new ForbiddenOperationException("当前优惠券活动不存在");
        }
        ActivityInfoResDTO activityInfoResDTO
                = BeanUtil.copyProperties(activity, ActivityInfoResDTO.class);

        //2. 根据活动id查询coupon统计当前活动的领取数量
        Integer count1 = couponService.lambdaQuery().eq(Coupon::getActivityId, id).count();
        activityInfoResDTO.setReceiveNum(count1);

        //3. 根据活动id查询coupon_write_off统计当前活动的核销数量
        Integer count2 = couponWriteOffService.lambdaQuery().eq(CouponWriteOff::getActivityId, id).count();
        activityInfoResDTO.setWriteOffNum(count2);

        return activityInfoResDTO;
    }

    /**
     * 撤销优惠卷活动
     *
     * @param id
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void revoke(Long id) {
        //对于待生效及进行中的活动如果要进行终止，可以执行撤销操作
        //执行撤销操作后此活动将终止，用户已抢到还未使用的优惠券将作废
        //有了活动ID，我首先先判断当前活动是不是待生效或者进行中的
        Activity activity = this.getById(id);
        if (ObjectUtil.isEmpty(activity)) {
            throw new ForbiddenOperationException("活动不存在");
        }
        if (activity.getStatus() != NO_DISTRIBUTE.getStatus() && activity.getStatus() != DISTRIBUTING.getStatus()) {
            throw new ForbiddenOperationException("当前活动状态不允许撤销");
        }
        //都满足的话，我们要再找到优惠卷的表，把优惠卷的表的状态也改成作废 所以这里涉及到两个表，活动表和优惠卷表,
        //其实我个人觉得，这里面就不需要找状态是1,2的活动了，因为iD是唯一的，不满足上述的状态他就不会走到下面了
        boolean flag = this.lambdaUpdate()
                .eq(Activity::getId, id)
                .set(Activity::getStatus, VOIDED.getStatus())
                .update();
        //还是加个判断吧，只有上面执行成功了，我们再进行下面的
        if (flag) {
            //这里要加入判断 因为怕客户用完优惠卷了
            couponService.lambdaUpdate()
                    .eq(Coupon::getActivityId, id)
                    .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())
                    .set(Coupon::getStatus, CouponStatusEnum.VOIDED.getStatus())
                    .update();
        }
    }

    /**
     * 定时任务-修改活动状态
     */
    @Override
    public void updateStatus() {
        //这个是活动状态到期自动转换
        //我们要判断全部符合条件的活动
        //1.没开始的活动，如果到了时间，我们给他开开
        //2.开始了的活动， 如果结束时间超时了，我们个他关了
        //好像没了
        //先1
        this.lambdaUpdate()
                .eq(Activity::getStatus, NO_DISTRIBUTE.getStatus())
                .lt(Activity::getDistributeStartTime, LocalDateTime.now())
                .gt(Activity::getDistributeEndTime, LocalDateTime.now())
                .set(Activity::getStatus, DISTRIBUTING.getStatus())
                .update();
        //后2
        this.lambdaUpdate()
                .in(Activity::getStatus, DISTRIBUTING.getStatus(), NO_DISTRIBUTE.getStatus())
                .lt(Activity::getDistributeEndTime, LocalDateTime.now())
                .set(Activity::getStatus, LOSE_EFFICACY.getStatus())
                .update();
    }
    @Override
    public void preHeat() {
        //1. 查询状态是待开始或者进行中，并且发放开始时间距离现在不足1个月的活动，按照开始时间升序排列
        //select * from activity where status in (1,2)  and distribute_start_time < 当前时间+1个月 order by distribute_start_time asc
        List<Activity> list = this.lambdaQuery()
                .in(Activity::getStatus, NO_DISTRIBUTE.getStatus(), DISTRIBUTING.getStatus())//status in (1,2)
                .lt(Activity::getDistributeStartTime, LocalDateTime.now().plusMonths(1))//distribute_start_time < 当前时间+1个月
                .orderByAsc(Activity::getDistributeStartTime)//order by distribute_start_time asc
                .list();
        if (CollUtil.isEmpty(list)) {
            list = new ArrayList<>();//防止缓存穿透
        }


        //2. 将查询到的数据封装到List<SeizeCouponInfoResDTO>，再转换为JSON串
        List<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS = BeanUtils.copyToList(list, SeizeCouponInfoResDTO.class);
        String jsonStr = JsonUtils.toJsonStr(seizeCouponInfoResDTOS);

        //3. 将JSON字符串存入redis
        redisTemplate.opsForValue().set(ACTIVITY_CACHE_LIST, jsonStr);

        //4. 将优惠券活动的库存从MySQL同步到Redis
        //4-1 对于待生效的活动更新库存
        list.stream().filter(e ->
                getStatus(e.getDistributeStartTime(), e.getDistributeEndTime(), e.getStatus()) == NO_DISTRIBUTE.getStatus()
        ).forEach(e ->
                redisTemplate.opsForHash().put(String.format(COUPON_RESOURCE_STOCK, e.getId() % 10), e.getId(), e.getStockNum())
        );

        //4-2 对于已生效的活动如果库存已经同步则不再同步
        list.stream().filter(e ->
                getStatus(e.getDistributeStartTime(), e.getDistributeEndTime(), e.getStatus()) == DISTRIBUTING.getStatus()
        ).forEach(e ->
                //只有库存不存在的情况下, 才要进行保存操作这个就是判断我有没有没存库存的情况
                redisTemplate.opsForHash().putIfAbsent(String.format(COUPON_RESOURCE_STOCK, e.getId() % 10), e.getId(), e.getStockNum())
        );
    }
    @Override
    public List<SeizeCouponInfoResDTO> queryForListFromCache(Integer tabType) {
        //1. 从Redis中查询优惠券活动的数据
        String jsonStr = (String) redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);
        if (StringUtils.isEmpty(jsonStr)) {
            return List.of();
        }

        //2. 将查询到的JSON字符串转换为集合List<SeizeCouponInfoResDTO>
        List<SeizeCouponInfoResDTO> seizeCouponInfoResDTOS
                = JSON.parseArray(jsonStr, SeizeCouponInfoResDTO.class);
        if (CollUtil.isEmpty(seizeCouponInfoResDTOS)) {
            return List.of();
        }

        //3. 根据tabType的值筛选符合条件的活动返回
        return seizeCouponInfoResDTOS.stream().filter(e -> {
            //获取当前获得真实的状态
            int status = getStatus(e.getDistributeStartTime(), e.getDistributeEndTime(), e.getStatus());
            if (tabType == 1) {//筛选疯抢中的
                return status == DISTRIBUTING.getStatus();
            } else {//筛选即将开始的
                return status == NO_DISTRIBUTE.getStatus();
            }
        }).map(e->{
            e.setRemainNum(e.getStockNum());
            return e;
        }).collect(Collectors.toList());
    }
    @Override
    public ActivityInfoResDTO getActivityInfoByIdFromCache(Long id) {
        //1. 缓存中获取活动信息
        String jsonString = (String) redisTemplate.opsForValue().get(ACTIVITY_CACHE_LIST);
        if (StringUtils.isEmpty(jsonString)) {
            return null;
        }

        //2. 字符串转换为集合
        List<ActivityInfoResDTO> activityInfoResDTOList = JSON.parseArray(jsonString, ActivityInfoResDTO.class);
        if (CollUtil.isEmpty(activityInfoResDTOList)) {
            return null;
        }

        //3. 过滤出指定id的活动
        return activityInfoResDTOList.stream()
                .filter(e -> e.getId().equals(id))
                .findFirst().orElse(null);
    }


    /**
     * 根据活动的目前状态、开始、结束时间 对比当前时间来获取到活动的真实状态
     * 1. 状态在待生效, 但是 活动开始时间 <=当前时间 < 活动结束时间  真实状态应该是 进行中
     * 2. 状态在待生效, 但是 活动结束时间 < 当前时间               真实状态应该是 已结束
     * 3. 状态在进行中, 但是 活动结束时间 < 当前时间               真实状态应该是 已结束
     * 4. 其它情况, 当前状态就是真实状态
     *
     * @param distributeStartTime 活动开始时间
     * @param distributeEndTime   活动结束时间
     * @param status              当前状态
     * @return 活动的真实状态
     */
    private int getStatus(LocalDateTime distributeStartTime, LocalDateTime distributeEndTime, Integer status) {
        if (NO_DISTRIBUTE.getStatus() == status && distributeStartTime.isBefore(DateUtils.now()) && distributeEndTime.isAfter(LocalDateTime.now())) {
            //状态在待生效, 但是 活动开始时间<=当前时间<活动结束时间  真实状态应该是 进行中
            return DISTRIBUTING.getStatus();
        } else if (NO_DISTRIBUTE.getStatus() == status && distributeEndTime.isBefore(LocalDateTime.now())) {
            //状态在待生效, 但是 活动结束时间 < 当前时间   真实状态应该是 已结束
            return LOSE_EFFICACY.getStatus();
        } else if (DISTRIBUTING.getStatus() == status && distributeEndTime.isBefore(LocalDateTime.now())) {
            //状态在进行中, 但是 活动结束时间 < 当前时间   真实状态应该是 已结束
            return LOSE_EFFICACY.getStatus();
        }
        return status;
    }
}
